﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using System.IO;
using System.Reflection;
using System.Data.SqlClient;

namespace LoggingQueue
{
    class Logged<T>
    {
        public T underlying { get; set; }
        public string file_path { get; set; }
    }

    public class LoggingQueue<T> : SqlReflectionStore<T>
    {
        private int batch_size;
        private int inserts_since_throttle;
        string directory = Path.Combine(Environment.CurrentDirectory, "LoggingQueue/" + typeof(T).Name + "/");
        Queue<Logged<T>> buffer = new Queue<Logged<T>>();
        PropertyInfo[] props = null;
        Timer timer;
        object state;

        public LoggingQueue(TimeSpan throttle, int batch_size)
        {
            this.batch_size = batch_size;
            this.props = typeof(T).GetProperties().Where(p => p.CanRead && p.CanWrite && (p.PropertyType.IsPrimitive || p.PropertyType.Equals(typeof(String)))).OrderBy(p => p.Name).ToArray();

            // intialize sql table
            base.InitSQL();

            // initialize flat file to store records
            this.InitFile();

            // timer callback will reset insertsSinceThrottle
            timer = new Timer(throttle.TotalMilliseconds);
            timer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
            timer.AutoReset = true;
        }

        public void Start()
        {
            timer.Start();
        }

        /// <summary>
        ///  on start, enqueue existing items from the file
        /// </summary>
        internal void InitFile()
        {
            DateTime startInit = DateTime.Now;
            if (!Directory.Exists(directory))
            {
                Directory.CreateDirectory(directory);
            }
            // read through all the files in the directory and load items into queue
            else
            {
                foreach (string name in Directory.GetFiles(directory))
                {
                    using (StreamReader sr = new StreamReader(GetStream(name)))
                    {
                        string line = null;
                        while ((line = sr.ReadLine()) != null)
                        {
                            char[] delimiters = new char[] { '\t' };
                            string[] parts = line.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
                            buffer.Enqueue(FromRecord(parts, name));
                        }
                    }
                }
            }
            DateTime endInit = DateTime.Now;
            double totalSeconds = endInit.Subtract(startInit).TotalSeconds;
            Console.WriteLine("Took " + totalSeconds.ToString() + " to read " + buffer.Count + " pending items into buffer");
        }

        /// <summary>
        /// Get stream for a specific file
        /// </summary>
        /// <param name="file_name"></param>
        /// <returns></returns>
        protected Stream GetStream(string full_path)
        {
            return File.Open(full_path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
        }

        /// <summary>
        /// Get a stream that is guaranteed to be writable, by creating a new file if necessary
        /// </summary>
        /// <returns></returns>
        protected Stream GetStream(out string file_name, int recursionCount = 0)
        {
            file_name = Guid.NewGuid().ToString();
            Stream stream = null;
            if (recursionCount > 9) { return stream; }

            try
            {
                stream = File.Open(directory + file_name, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
                return stream;
            }
            catch (Exception ex)
            {
                return GetStream(out file_name, ++recursionCount);
            }
        }

        /// <summary>
        ///  read from array of fields in alphabetical order by property name
        /// </summary>
        /// <param name="fields"></param>
        /// <returns></returns>
        internal Logged<T> FromRecord(string[] fields, string file_path)
        {
            ConstructorInfo constructor = typeof(T).GetConstructor(new Type[] { });
            T instance = (T)constructor.Invoke(null);
            for (int i = 0; i < props.Length; i++)
            {
                PropertyInfo p = props[i];
                object val = Convert.ChangeType(fields[i], p.PropertyType);
                p.SetValue(instance, val, null);
            }
            return new Logged<T>()
            {
                underlying = instance,
                file_path = file_path
            };
        }

        /// <summary>
        /// Add item to flat file and enqueue in memory to insert to db later
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        internal Logged<T> WriteToFile(T item)
        {
           string file_path = "";
                using (StreamWriter sw = new StreamWriter(GetStream(out file_path)))
                {
                    sw.Write(ToRecord(item));
                    sw.Write(Environment.NewLine);
                    return new Logged<T>()
                    {
                        underlying = item,
                        file_path = directory + file_path
                    };
                }
        }

        protected string ToRecord(T item)
        {
            StringBuilder s = new StringBuilder();
            foreach (var prop in props)
            {
                //ignore read or write only props as well as objects/collections
                object val = prop.GetValue(item, null);
                if (val == null)
                {
                    s.Append("null\t");
                }
                else
                {
                    s.Append(val.ToString() + "\t");
                }
            }
            return s.ToString();
        }

        /// <summary>
        /// Add item to flat file and enqueue in memory to insert to db later
        /// </summary>
        /// <param name="dataObject"></param>
        /// <returns></returns>
        public bool Append(T dataObject)
        {
            Logged<T> item = this.WriteToFile(dataObject);
            if (item == null)
            {
                return false;
            }
            else
            {
                this.buffer.Enqueue(item);
                return true;
            }
        }

        /// <summary>
        /// add items to sql db
        /// </summary>
        /// <param name="dataObject"></param>
        /// <returns></returns>
        protected bool InsertNext()
        {
            if (buffer.Count < 1) { return false; }
            if (inserts_since_throttle >= batch_size) { return false; }
            Logged<T> o = buffer.Dequeue();
            bool add = base.Add(o.underlying);
            if (add)
            {
                inserts_since_throttle++;
                bool removed = Remove(o);
            }

            return add;
        }

        /// <summary>
        /// Remove item from flat file
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        bool Remove(Logged<T> item)
        {
            string record = ToRecord(item.underlying);

            // since this keeps creating new files, each file prob wouldn't be too big to do this
                var all_lines = new List<String>(File.ReadAllLines(item.file_path));
                int index = all_lines.IndexOf(record);
                if (index >= 0)
                {
                    all_lines.RemoveAt(index);
                }
                if (all_lines.Count == 0) // delete the file
                {
                    File.Delete(item.file_path);
                }
                else
                {
                    File.WriteAllLines(item.file_path, all_lines);
                }
                return true;
        }

        // timer callback delegate
        public void OnTimedEvent(object state, ElapsedEventArgs e)
        {
            timer.Stop();
            DateTime startAdding = DateTime.Now;
            this.inserts_since_throttle = 0;

            bool inserted = false;
            do
            {
                inserted = InsertNext();
            }
            while (inserted == true);
            DateTime endAdding = DateTime.Now;
            double totalSeconds = endAdding.Subtract(startAdding).TotalSeconds;
            Console.WriteLine("Took " + totalSeconds.ToString() + " to add " + batch_size + " items to database");
            timer.Start();
        }

        public void Dispose()
        {
            timer.Dispose();
        }


    }
}
